""" sbak/entity/common.py - Provides commonly-needed entity types. """

import operator

from sbak.vector import Vector
from sbak import video
from sbak.draw import image as draw_image

from sbak.entity.base import Entity, Actor

__all__ = 'Background', 'Camera'

class Background(Entity):
    """ An entity that acts like a background image.
        
        Backgrounds support "parallaxing," or being drawn such that they appear
        to be nearer or farther away from other objects. This is done using a
        pair of "parallax factors," one for each axis.
        
        Backgrounds also support repeated drawing. You can specify how many times
        an image is drawn along each axis by giving a "repeat factor."
        
        Finally, Background never collide with anything, regardless of their
        bounding box or groups.
        
        Attributes:
        
        plx:    The parallax factors.
                Must be a sequence of 2 numbers. For each number, 1 is
                considered the default, and it effectively disables parallaxing.
                The closer a number gets to 0, the farther away the image
                will appear. At 0 it appears to be infinitely distant. Negative
                numbers make the image "compete against" everything else. (You
                have to see to understand.)
                Numbers above 1 make the image appear to be in the foreground,
                assuming the layer of the background is higher than all other
                objects.
                
        repeat: The repeat count for both axes.
                Must be a sequence of 2 integers. For each number, 1 is
                considered the default, and effectively disables repeating.
                The number 0 is treated as infinite repetition.
    """
    
    tag = 'background'
    
    def __init__(self,  id=None,
                        world=None,
                        pos=(0,0),
                        image=None,
                        layer=0,
                        plx=None,
                        repeat=None,
                        *vargs,
                        **kwargs):
        """ Initializes a new Background entity.
        
            id:     Entity ID.
            world:  Initial world.
            pos:    Initial position within the world.
            image:  Initial image for the background.
            layer:  Layer at which the background will appear.
            plx:    Initial parallax factors.
            repeat: Initial repeat factors.
        """
        super(Background,self).__init__(
            id = id,
            world = world,
            image = image,
            pos = pos,
            layer = layer
            *vargs,
            **kwargs
        )
        self.plx = plx or [1,1]
        self.repeat = repeat or [1,1]
    
    def draw(self,surf,offset=(0,0),area=None):
        """ Draws the background with repeat and parallaxing.
            This method effectively works the same way as tile.TileMap.draw
            except that every tile is the same image and some parallaxing
            factors are taken into account.
        """
        img = self.image
        if img is not None:
            # Localize some stuff
            maxMX,maxMY = self.repeat
            imgW,imgH = img.get_size()
            plxX,plxY = self.plx
            # Find the map area to draw
            if area is not None:
                area = Rect(area).move([-v for v in self._pos])
                area.left, area.top = area.left*plxX, area.top*plxY
                px,py,pw,ph = area  # Pixel area
                # Map area
                (mx,my),(mw,mh) = ((int(px/imgW), int(py/imgH)),
                                  (int(ceil(pw*1.0/imgW))+1,
                                   int(ceil(ph*1.0/imgH))+1))
                # Base drawing offset
                bdx,bdy = map(sum, zip(offset,[-v for v in area[0:2]]))
            else:
                # Map area
                (mx,my),(mw,mh) = (0,0),self.repeat
                # Base drawing offset
                bdx,bdy = sum_multi(self._pos,offset)
            # Draw each tile in the area
            for o in xrange(my,mh+my):
                for i in xrange(mx,mw+mx):
                    if ((not maxMX or (i>=0 and i<maxMX)) and
                        (not maxMY or (o>=0 and o<maxMY))):
                        draw_image(img, surf, (bdx+(i*imgW), bdy+(o*imgH)) )

class Camera(Actor):
    """ An actor that displays its world to a surface.
    
        A Camera is a special kind of actor that is used to display
        worlds. It has an extra drawing method, `display`, which
        draws everything in the world that is touching or inside of
        the camera's occupied region.
        
        ~~~ Attributes ~~~
        
        displayhook:The hook created to make the camera display when
                    part of a game. If not None this should not be
                    changed, but the displayhook.remove() method may be
                    called to implicitly and safely set the displayhook
                    to None.
    """
    
    tag = 'camera'
    
    def __init__(self,
                    id = None,
                    bbox = None,
                    offset = (0,0),
                    clip = True,
                    leader = None,
                    dpriority = 0,
                    *vargs, **kwargs):
        """ Initializes and registers a new Camera object.
            
            bbox:   Boundry box of the camera in the form of a Pygame
                    rect. If no bbox is passed, a new bbox will be
                    generated with position 0,0 and size equal to the
                    size of the current screen.
            
            offset: When displaying the camera's scene, the camera will
                    draw on the target surface with this offset. Pass a
                    vector or (x,y) sequence. (Default 0,0)
            
            clip:   If true, clipping will be applied when drawing the
                    camera's scene. (Default True.)
                    
            leader: Entity the camera should follow. May be an entity
                    object or entity ID.
            
            dpriority:
                    Display priority. This will become the priority of
                    the draw hook created when the camera is added to
                    a game.
                    
            Unrecognized arguments are passed along to the Entity
            constructor unprocessed.
        """
        
        # Determine what bbox to use
        if bbox is None:
            bbox = ((0,0), video.get_screen().get_size())
            
        # Assign default values
        self._leader = None
        self.displayhook = None # Hook to CE_DISPLAY control event
        self._clip = False
        self._offset = Vector()
        
        # Input values from constructor
        self.offset = offset
        self.clip = clip
        if leader is not None:
            self.set_leader(leader)
            
        super(Camera,self).__init__(
            id=id,
            bbox=bbox,
            *vargs,
            **kwargs
        )
    
    def set_offset(self, val):
        self._offset = Vector(val)
    offset = property(
        fget = operator.attrgetter('_offset'),
        fset = set_offset,
        doc =
        """ Display surface draw position.
            
            This is a vector that indicates the offset on the target
            surface on which the camera's scene is drawn whenever its
            `display` method is triggered. It is guarunteed to be a
            Vector, even if another type of object is assigned to it.
        """
    )
    _rep_offset = lambda self: self._offset
    
    def set_clip(self, val):
        self._clip = bool(val)
    clip = property(
        fget = operator.attrgetter('_clip'),
        fset = set_clip,
        doc =
        """ Whether or not clipping is applied when displaying.
        
            This will always be a bool, even when another type of
            object is assigned to it.
        """
    )
    _rep_clip = lambda self: self._clip
    
    def set_game_internal(self, game):
        """ Registers a camera with a game without telling the game.
        
            This is the same as Actor.set_game_internal, but it creates
            a CE_DISPLAY hook on the game's event manager. If the hook
            still remains after the camera is removed from the game,
            the hook is removed as well.
        """
        super(Camera, self).set_game_internal(game)
        
        pri = 0
        if self.displayhook is not None:
            pri = self.displayhook.priority
            self.displayhook.remove()
        
        if game is not None:
            self.displayhook = self.hook_weak(
                callback = self.display,
                type = 'controlevent',
                control = 'display',
                priority = pri
            )
            self.displayhook.on_remove.append(self._remove_displayhook)
    
    def _remove_displayhook(self, hook):
        self.displayhook = None
    
    _rep_dpriority = (lambda self: self.displayhook.priority
                      if self.displayhook is not None
                      else 0)
    
    def update(self, e=None):
        """ Updates the camera.
            
            For the basic camera this merely places the camera at the
            position of the entity it is following (if it is following one
            at all) and moves the camera inside the world.
            
            The camera will follow an entity into whatever world it enters.
            However, it will not follow an entity into "nowhere" (world=None)
            and if the entity goes there the world will merely wait for the
            entity to re-enter a world.
            
            Also causes the camera to stop following an entity if it is dead.
        """
        super(Camera,self).update()
        wld = self.world
        # Following entities
        if self._leader is not None:
            # Follow entity into other worlds but don't follow it to nowhere
            if self._leader.world is not None:
                wld = self._leader.world
                self.pos = self._leader.pos
        # Put values back in
        if self.world is not wld:
            self.world = wld
        # Staying within the world
        if wld is not None:
            wW,wH = wld.get_region()[2:4]
            if self.left < 0: self.left = 0
            if self.right > wW-1: self.right = wW-1
            if self.top < 0: self.top = 0
            if self.bottom > wH-1: self.bottom = wH-1

    def kill(self):
        """ Kills the camera.
        
            In addition to the usual entity death behavior this also
            causes the camera to stop following whatever entity it is
            following.
        """
        self.set_leader(None)
        super(Camera,self).kill()
    
    def set_leader(self,ent):
        """ Start following an entity.
            
            Pass None to stop following. (Same as unset_leader.)
            
            Following an entity implicitly adds a handler to the
            on_kill psuedo-event handler list.
        """
        # If there is already a leader, unhook from that leader.
        if self._leader is not None:
            self._leader.on_kill.remove(self.unset_leader)
            
        # If the new leader is not None
        if ent is not None:
            # Put self into the same world as the new leader.
            # (This is needed to make sure the camera actually gets updated)
            if ent.world is not None:
                self.world = ent.world
                
            # Make sure the camera stops following when the leader dies.
            ent.on_kill.append(self.unset_leader)
            
        # Set the new leader
        self._leader = ent
    
    def unset_leader(self):
        """ Equivalent to set_leader(None).
            Used as an on_hook handler by the current leader.
        """
        self._leader = None
    
    def get_leader(self):
        """ Returns the entity that we're following. """
        return self._leader
    
    def display(self, e=None):
        """ Handles CE_DISPLAY by drawing a portion of the world
        
            This is Camera's event handler for the `CONTROLEVENT` event
            with `control` of CE_DISPLAY. It uses the `dest` member of
            the event as a destination surface to which it draws the
            region of the world that it occupies. The position of the
            dest surface to which the scene is drawn is determined
            by the camera's `offset` property, and if the `clip`
            property is true, then clipping will be used to prevent
            drawing anything outside of the region of the surface
            with size equal to the size of the camera's bbox.
            
            If no e is passed, the screen surface is used.
        """
        if self.world is not None:
            try:
                dest = e.dest
            except AttributeError:
                dest = dest or video.get_screen()
            
            o = self.offset
            
            clipr = dest.get_clip()
            if self.clip:
                dest.set_clip(Rect(o, self.size))
            
            self.world.draw(dest, o, self.region)
            
            dest.set_clip(clipr)
